/*
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Topless JSF.
 *
 * The Initial Developer of the Original Code is
 * Marco Antonio Villalobos, Jr. (mvillalobos@kineteque.com).
 * Portions created by the Initial Developer are Copyright (C) 2010
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Marco Antonio Villalobos, Jr. (mvillalobos@kineteque.com).
 *
 * ***** END LICENSE BLOCK *****
 */

package kineteque.jsf.topless.components.table;

import kineteque.jsf.topless.util.CaseConverter;
import kineteque.jsf.topless.jdbc.ToplessJdbcTemplate;
import kineteque.jsf.topless.jdbc.PaginatedList;
import org.apache.log4j.Logger;

import javax.faces.component.UINamingContainer;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIData;
import javax.faces.event.ActionEvent;
import javax.faces.context.FacesContext;
import javax.el.ValueExpression;
import javax.sql.DataSource;
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

/**
 * The topless table container.
 */
@FacesComponent(value = "kineteque.jsf.table")
public class UITable extends UINamingContainer {

    /**
     * The servlet context parameter which provides the data source JNDI name.
     * It has the value &quot;topless.DATA_SOURCE_JNDI_NAME&quot;.
     */
    public final static String DATA_SOURCE_CONTEXT_JNDI_NAME = "topless.DATA_SOURCE_JNDI_NAME";

    /** The logger for this class. */
    private final static Logger logger = Logger.getLogger(UITable.class);

    /**
     * Retrieves the family for this component.  Since this is
     * a composite component, it must return &quot;javax.faces.NamingContainer&quot;.
     *
     * @return The family for this component.
     */
    @Override
    public String getFamily() {
        return "javax.faces.NamingContainer";
    }

    /** The jdbc template */
    protected ToplessJdbcTemplate toplessJdbc = null;

    /** The data-structure for the displayed data used by this component. */
    protected PaginatedList<Map<String,Object>> value = null;

    /** The jdbc data-source. */
    protected DataSource dataSource = null;

    /** The sql query that this component uses to retrieve data from the data-source. */
    protected String query = null;

    /** The sql query parameters in name and value pairs that will be injected into the sql named parameters. */
    protected Map<String,Object> parameters = null;

    /** The list of columns to include. */
    protected List<String> includes = null;

    /** The list of columns to exclude. */
    protected List<String> excludes = null;

    /** A map for renaming the displayed column names. */
    protected Map<String,String> columnNames = null;

    /** The first row to display. */
    protected Integer first = null;

    /** The number of rows to display. */
    protected Integer rows = null;

    /** A reference to the UIData table that this component wraps. */
    protected UIData table = null;

    /** The column to order by. */
    protected String orderBy = null;

    /** The order to sort by type. */
    protected enum Order {
        /** Sort ascending. */
        ASC,
        /** Sort descending. */
        DESC
    }

    /** The order to sort by value.  Default value is ascending. */
    protected Order order = Order.ASC;

    /**
     * Retrieves the wrapped UIData table.
     *
     * @return The wrapped UIData table.
     */
    public UIData getTable() {
        if (table!=null) {
            return table;
        }
        ValueExpression ve = getValueExpression("table");
        return ve != null ? (UIData) ve.getValue(getFacesContext().getELContext()) : null;
    }

    /**
     * Assigns the wrapped UIData table.
     *
     * @param table The wrapped UIData table.
     */
    public void setTable(UIData table) {
        this.table = table;
    }

    /**
     * Retrieves the jdbc data-source.
     *
     * If the data-source is not stored as a value or value expression, this will optionally lookup
     * the servlet context parameter with the name &quot;topless.DATA_SOURCE_JNDI_NAME&quot;.
     *
     * Then that value is used to lookup a data-source, which will be cached.
     *
     * @return The jdbc data-source.
     */
    public DataSource getDataSource() {
        if (dataSource!=null) {
            return dataSource;
        }
        ValueExpression ve = getValueExpression("dataSource");
        if (ve != null) {
            return (DataSource) ve.getValue(getFacesContext().getELContext());
        } else {
            try {
                String jndiName = getFacesContext().getExternalContext().getInitParameter(DATA_SOURCE_CONTEXT_JNDI_NAME);
                if (jndiName!=null && jndiName.trim().length()>0) {
                    Context env;
                    Context initial = new InitialContext();
                    env = (Context)initial.lookup("java:/comp/env");
                    dataSource = (DataSource) env.lookup(jndiName);
                    return dataSource;
                }
            }
            catch (Exception e) {
                throw new RuntimeException(new StringBuilder()
                    .append("Could not find DataSource with Context Parameter name: ")
                    .append(DATA_SOURCE_CONTEXT_JNDI_NAME).append(".").toString(),e);
            }
        }
        //this is unreachable
        return null;
    }

    /**
     * Assigns the jdbc data-source.
     *
     * @param dataSource The jdbc data-source.
     */
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * Retrieves the topless jdbc template.
     *
     * @return The topless jdbc template.
     */
    protected ToplessJdbcTemplate getToplessJdbc() {
        if (toplessJdbc==null) {
            toplessJdbc = new ToplessJdbcTemplate(getDataSource());
        }
        return toplessJdbc;
    }

    /**
     * Retrieves the sql query that this component uses to retrieve data from the data-source.
     * The query can contain named parameters, for example &quot;SELECT * FROM widgets WHERE :price &gt; 250&quot;.
     * The parameter &quot;:price&quot; will be assigned through the parameters attribute.
     *
     * @return The sql query.
     */
    public String getQuery() {
        if (query!=null) {
            return query.trim();
        }
        ValueExpression ve = getValueExpression("query");
        if (ve != null) {
            String q = (String) ve.getValue(getFacesContext().getELContext());
            return q==null ? null : q.trim();
        } else {
            return null;
        }
    }

    /**
     * Assigns the sql query that this component uses to retrieve data from the data-source.
     * The query can contain named parameters, for example &quot;SELECT * FROM widgets WHERE :price &gt; 250&quot;.
     * The parameter &quot;:price&quot; will be assigned through the parameters attribute.
     *
     * @param query The sql query.
     */
    public void setQuery(String query) {
        this.query = query;
    }

    /**
     * Retrieves the sql query parameters in name and value pairs that will be injected into the sql named parameters.
     *
     * @return The sql query parameters.
     */
    public Map<String, Object> getParameters() {
        if (parameters!=null) {
            return parameters;
        }
        ValueExpression ve = getValueExpression("parameters");
        return ve != null ? (Map<String, Object>) ve.getValue(getFacesContext().getELContext()) : null;
    }

    /**
     * Assigns the sql query parameters in name and value pairs that will be injected into the sql named parameters.
     *
     * @param parameters The sql query parameters.
     */
    public void setParameters(Map<String, Object> parameters) {
        this.parameters = parameters;
    }

    /**
     * Retrieves the value, which is a paginated list.
     *
     * If the paginated list is not stored as a value or value expression, then this
     * will use the first, row, query, parameters, jdbc template, and data-source attributes to retrieve the paginated list
     * from the data-source.
     *
     * The result will then be stored in the value.
     *
     * @return The value, which is a paginated list.
     */
    public PaginatedList<Map<String,Object>> getValue() {
        if (value !=null) {
            if (logger.isTraceEnabled()) {
                logger.trace(new StringBuilder().append("Retrieving cached value in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
            }
            return value;
        }
        ValueExpression ve = getValueExpression("value");
        if (ve != null) {
            if (logger.isTraceEnabled()) {
                logger.trace(new StringBuilder().append("Retrieving value through value expression in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
            }
            return (PaginatedList<Map<String,Object>>) ve.getValue(getFacesContext().getELContext());
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace(new StringBuilder().append("Retrieving value through query in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
            }
            String query = getQuery();
            if (query.startsWith("<![CDATA[") && query.endsWith("]]>")) {
                query = query.substring(9,query.length()-3);
            }
            ToplessJdbcTemplate topless = getToplessJdbc();
            String f;
            if (orderBy!=null && orderBy.trim().length()!=1) {
                f = new StringBuilder(query).append(" ORDER BY ").append(orderBy).append(' ').append(order).toString();
            } else {
                f = query;
            }
            Map<String,Object> p = getParameters();
            value = topless.queryForList(f, p, getFirst(), getRows());
            return value;
        }
    }

    /**
     * Assigns the value, which is a paginated list.
     *
     * @param value The value, which is a paginated list.
     */
    public void setValue(PaginatedList<Map<String,Object>> value) {
        this.value = value;
    }

    /**
     * Retrieves the list of columns from the result set to include in the view.
     * If it is not specified, then all of the columns that are not in the exclude list will be displayed.
     *
     * @return The list of columns from the result set to include in the view.
     */
    public List<String> getIncludes() {
        if (includes != null) {
            if (logger.isTraceEnabled()) {
                logger.trace(new StringBuilder().append("Retrieving cached includes in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
            }
            return includes;
        } else {
            ValueExpression ve = getValueExpression("includes");
            List<String> includes = null;
            if (ve != null) {
                if (logger.isTraceEnabled()) {
                    logger.trace(new StringBuilder().append("Retrieving includes through value expression in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
                }
                includes = (List<String>) ve.getValue(getFacesContext().getELContext());
            } else {
                if (logger.isTraceEnabled()) {
                    logger.trace(new StringBuilder().append("Retrieving includes through value lookup in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
                }
                PaginatedList<Map<String, Object>> value = getValue();
                return value !=null ? value.getColumnNames() : null;
            }
            return includes;
        }
    }

    /**
     * Assigns the list of columns from the result set to include in the view.
     *
     * @param includes The list of columns from the result set to include in the view.
     */
    public void setIncludes(List<String> includes) {
        this.includes = includes;
    }

    /**
     * Retrieves the list of columns from the result set to exclude in the view.
     *
     * @return The list of columns from the result set to exclude in the view.
     */
    public List<String> getExcludes() {
        if (excludes !=null) {
            return excludes;
        }
        ValueExpression ve = getValueExpression("excludes");
        if (ve != null) {
            return (List<String>) ve.getValue(getFacesContext().getELContext());
        } else {
            return null;
        }
    }

    /**
     * Assigns the list of columns from the result set to exclude in the view.
     *
     * @param excludes The list of columns from the result set to exclude in the view.
     */
    public void setExcludes(List<String> excludes) {
        this.excludes = excludes;
    }

    /**
     * Retrieves the list of column names as they will be presented to the user.
     * This will first attempt to retrieve the assigned value.  If it is not there, then it will
     * attempt to retriev the value expression.  If it is not there, then it will retrieve the included
     * column names and convert them into a human readable format.
     *
     * @return The list of column names as they will be presented to the user.
     */
    public Map<String, String> getColumnNames() {
        if (columnNames!=null) {
            if (logger.isTraceEnabled()) {
                logger.trace(new StringBuilder().append("Retrieving cached column names in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
            }
            return columnNames;
        } else {
            ValueExpression ve = getValueExpression("columnNames");
            if (ve != null) {
                if (logger.isTraceEnabled()) {
                    logger.trace(new StringBuilder().append("Retrieving column names through value expression in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
                }
                return (Map<String, String>) ve.getValue(getFacesContext().getELContext());
            } else {
                if (logger.isTraceEnabled()) {
                    logger.trace(new StringBuilder().append("Retrieving column names through includes lookup in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
                }
                columnNames = new HashMap<String, String>();
                for (String name : getIncludes()) {
                    String human = CaseConverter.tableToHuman(name);
                    columnNames.put(name, human);
                }
                //to do - place in request context and cache per request
                return columnNames;
            }
        }
    }

    /**
     * Assigns the list of column names as they will be presented to the user.
     *
     * @param columnNames The list of column names as they will be presented to the user.
     */
    public void setColumnNames(Map<String, String> columnNames) {
        this.columnNames = columnNames;
    }

    /**
     * Retrieves the first row attribute.
     *
     * The first attribute represents which row within the entire list to display in the first row.
     *
     * The first attribute can be assigned a value binding expression.
     *
     * @return The first row attribute.
     */
    public Integer getFirst() {
        if (first!=null) {
            return first;
        }
        ValueExpression ve = getValueExpression("first");
        return ve != null ? (Integer) ve.getValue(getFacesContext().getELContext()) : null;
    }

    /**
     * Assigns the first row attribute.
     *
     * @param first The first row attribute.
     */
    public void setFirst(Integer first) {
        this.first = first;
    }

    /**
     * Retrieves the amount of rows to display.
     *
     * The rows attribute can be assigned a value binding expression.
     *
     * @return The amount of rows to display.
     */
    public Integer getRows() {
        if (rows!=null) {
            return rows;
        }
        ValueExpression ve = getValueExpression("rows");
        return ve != null ? (Integer) ve.getValue(getFacesContext().getELContext()) : null;
    }

    /**
     * Assigns the amount of rows to display.
     *
     * @param rows The amount of rows to display.
     */
    public void setRows(Integer rows) {
        this.rows = rows;
    }

    /**
     * Retrieves the total rows that match the given query.
     *
     * Please note that the total rows is used for paging.  It is not the amount of rows displayed.
     * The total rows value is stored in the paginated list.  If the paginated list is NULL then this will return 0.
     *
     * @return The total rows that match the given query.
     */
    public int getTotalRows() {
        return value !=null ? value.getRows() : 0;
    }

    /**
     * The first action event handler.
     *
     * This moves the cursor to the first set of rows in the complete result set.
     *
     * @param e The action event.
     */
    public void first(ActionEvent e) {
        first = 0;
        value = null;
    }

    /**
     * The previous action event handler.
     *
     * This moves the cursor to the previous set of rows in the complete result set.
     * For example, if there 1000 total rows, and 10 rows displayed, the user is looking at rows
     * 50 through 59, then previous will move the cursor to view rows 40 through 49.
     *
     * @param e The action event handler.
     */
    public void prev(ActionEvent e) {
        Integer f = getFirst();
        int first = f==null ? 0 : f;
        Integer r = getRows();
        int rows = r==null ? 0 : r;
        first -= rows;
        if (first<0) {
            first = 0;
        }
        this.first = first;
        value = null;
    }

    /**
     * The next action event handler.
     *
     * This moves the cursor to the next set of rows in the complete result set.
     * For example, if there 1000 total rows, and 10 rows displayed, the user is looking at rows
     * 50 through 59, then next will move the cursor to view rows 60 through 69.
     * 
     * @param e The action event.
     */
    public void next(ActionEvent e) {
        Integer f = getFirst();
        int first = f==null ? 0 : f;
        Integer r = getRows();
        int rows = r==null ? 0 : r;
        int totalRows = value ==null ? 0 : value.getRows();
        first += rows;
        if (first>=totalRows) {
            first = totalRows - rows;
            if (first<0) {
                first = 0;
            }
        }
        this.first = first;
        value = null;
    }

    /**
     * The last action event handler.
     *
     * * This moves the cursor to the last set of rows in the complete result set.
     *
     * @param e The action event.
     */
    public void last(ActionEvent e) {
        Integer r = getRows();
        int rows = r==null ? 0 : r;
        int totalRows = value ==null ? 0 : value.getRows();
        first = totalRows - rows;
        if (first<0) {
            first = 0;
        }
        value = null;
    }

    /**
     * The action event handler for specifying which column to sort by.
     *
     * If the same row is specified, the sort order is toggled, maintaining the same first row value..
     * If columns are changed, then the sort order is defaulted to ascending and the first row is displayed.
     *
     * @param e The action event handler.
     */
    public void orderBy(ActionEvent e) {
        String c = (String)e.getComponent().getAttributes().get("c");
        if (!c.equals(orderBy)) {
            orderBy = c;
            order = Order.ASC;
            first = 0;
        } else {
            order = order==Order.ASC ? Order.DESC : Order.ASC;
        }
        value = null;
    }

    /**
     * Retrieves the name of the that this table is currently sorting by.
     *
     * @return The name of the that this table is currently sorting by.
     */
    public String getOrderBy() {
        return orderBy;
    }

    /**
     * Retrieves the sort order.
     *
     * @return The sort order.
     */
    public String getOrder() {
        return order.toString();
    }

    /**
     * Saves the state for this component.
     *
     * @param ctx The faces context for this request.
     * @return An array of saved state values.
     */
    public Object saveState(FacesContext ctx) {
        if (logger.isTraceEnabled()) {
            logger.trace(new StringBuilder().append("Save state in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
        }
        Object values[] = new Object[10];
        values[0] = super.saveState(ctx);
        values[1] = first;
        values[2] = rows;
        values[3] = orderBy;
        values[4] = order;
        values[5] = value;
        values[6] = dataSource;
        values[7] = parameters;
        values[8] = includes;
        values[9] = columnNames;
        return values;
    }

    /**
     * Restores the state for this component.
     *
     * @param ctx The faces context for this request.
     * @param state The state object to restore from.
     */
    public void restoreState(FacesContext ctx, Object state) {
        if (logger.isTraceEnabled()) {
            logger.trace(new StringBuilder().append("Restore state in phase = ").append(getFacesContext().getCurrentPhaseId()).append(".").toString());
        }
        Object values[] = (Object[]) state;
        super.restoreState(ctx,values[0]);
        first = (Integer)values[1];
        rows = (Integer)values[2];
        orderBy = (String)values[3];
        order = (Order)values[4];
        value = (PaginatedList<Map<String,Object>>)values[5];
        dataSource = (DataSource)values[6];
        parameters = (Map<String,Object>)values[7];
        includes = (List<String>) values[8];
        columnNames = (Map<String, String>) values[9];
    }
    
    public void remove(Map<String,Object> item) {
        if (value!=null && item!=null) {
            value.getList().remove(item);
        }
    }
    
}
